﻿function ConvertTo-Instance
{
    [cmdletbinding(DefaultParameterSetName="none")]
    Param( 
        [Parameter(Mandatory=$true,ValueFromPipeline=$false)]
        [PsObject] 
        $Template,

        [Parameter(Mandatory=$true,ValueFromPipeline=$false)]
        [PsObject] 
        $DataContext,

        [Parameter(Mandatory=$false,ValueFromPipeline=$false)]
        [Switch] 
        $DataContextAlreadySwitch,

        [Parameter(ParameterSetName="ChildObject",Mandatory=$true,ValueFromPipeline=$false)]
        [Parameter(ParameterSetName="AddToProperty",Mandatory=$true,ValueFromPipeline=$false)]
        [PsObject] 
        $ParentObject,

        [Parameter(ParameterSetName="AddToProperty",Mandatory=$true,ValueFromPipeline=$false)]
        [PsObject] 
        $ParentPropertyName
        )

    Process
    {

        #Switch or not the Datacontext if template has a specific datacontext property
        if ($Template.dataContext -and (-not $DataContextAlreadySwitch))
        {
            Write-Verbose "ConvertTo-Instance - switch DataContext to '$($Template.dataContext)' from current DataContext '$DataContext'"

            $bindingSourcePropertyName = $Template.dataContext
            Write-Debug "Copy-MemberFromTemplate - bindingSourcePropertyName : $bindingSourcePropertyName"

            $existMember = Get-Member -InputObject $DataContext -Name $bindingSourcePropertyName
            if (-not $existMember)
            {
                throw "Property '$bindingSourcePropertyName' doesn't exists on current Datacontext '$DataContext'"
            }

            $DataContext = $DataContext.$bindingSourcePropertyName
            Write-Debug "Copy-MemberFromTemplate - bindingSourcePropertyCollections : $DataContext"

        }


        #Check if the binding is a multi-binding based on a collection or a single binding
        $dataContextType = $DataContext.GetType().Name
        Write-Debug "ConvertTo-Instance - DataContext is a type : '$dataContextType'"
        Write-Verbose "ConvertTo-Instance - DataContext is a type : '$dataContextType'"

        if ($dataContextType -eq "PSCustomObject")
        {
            Write-Verbose "DataContext is a Single binding : '$($DataContext.GetType())'"
            #Prepare the Binding Collection for this object
            if ($PSCmdlet.ParameterSetName -eq "ChildObject")
            {
                $BindingCollections = $ParentObject.BindingCollections
            }
            else
            {
                $BindingCollections = @{}
            }

            $objectInstance = New-Object PSObject
            $objectInstance | Add-Member -MemberType NoteProperty -Name BindingSource -Value $DataContext
            $objectInstance | Add-Member -MemberType NoteProperty -Name BindingCollections -Value $BindingCollections
            Write-Debug "ConvertTo-Instance - InstanceObject Initialisation : $objectInstance"
		    Copy-MemberFromTemplate -ObjectInstance $objectInstance -Template $Template
            Write-Debug "ConvertTo-Instance - InstanceObject Resultat : $objectInstance"
        
        }
        else
        {
            Write-Verbose "DataContext is a Multi binding : '$($DataContext.GetType())'"
            Write-Verbose "ConvertTo-Instance - DataContext is a type : '$dataContextType'"
            
            $objectInstance = @()
            foreach($bindingSourceElement in $DataContext)
            {
                if ($PSCmdlet.ParameterSetName -eq "ChildObject")
                {
                    $value = ConvertTo-Instance -Template $Template -DataContext $bindingSourceElement -ParentObject $ParentObject -DataContextAlreadySwitch
                }
                else
                {
                    $value = ConvertTo-Instance -Template $Template -DataContext $bindingSourceElement -DataContextAlreadySwitch
                }
                $objectInstance += $value
            }                                
        }

        #Prepare the Binding Collection for this object
        if ($PSCmdlet.ParameterSetName -eq "AddToProperty")
        {
            #Check the property Target Type
            $targetPropertyType = $null
            $existTargetProperty = Get-Member -InputObject $ParentObject -Name $ParentPropertyName
            if ($existTargetProperty)
            {
                $targetPropertyType = $ParentObject.$ParentPropertyName.getType().Name
                #Check the target Property Type
                switch($targetPropertyType)
                {
                    "Object[]"
                    {
                        #The target property type is an arraw, we jsut have to add the bound result to this array
                        $ParentObject.$ParentPropertyName += $objectInstance
                    }
                    "Hashtable"
                    {
                        #The target property type is a hash table, we have to browse the collection af bound result to add them to the hashtable
                        #if bound result doesn't have a uniquekey property, we throw an error
                        $objectInstance | ForEach-Object {
                            $simpleObject = $_
                            if (-not $simpleObject.uniqueKey)
                            {
                                throw "Unable to add an object in a hashtable without a property uniqueKey. Object beign added is : $simpleObject"
                            }
                            $ParentObject.$ParentPropertyName.add($simpleObject.UniqueKey, $simpleObject)
                        }
                        
                    }
                    default
                    {
                        $ParentObject.$ParentPropertyName = $objectInstance
                    }

                }

            }
            else
            {
                #Target property doesn't exist, so we can add directly the bound result as the parentProperty Name
                $ParentObject | Add-Member -MemberType NoteProperty -Name $ParentPropertyName -Value $objectInstance                
            }
        }
        else
        {
            $objectInstance
        }

    }
}


function Copy-MemberFromTemplate
{
   [cmdletbinding(DefaultParameterSetName="none")]
    Param( 
        [Parameter(Mandatory=$true,ValueFromPipeline=$false)]
        [PsObject] 
        $ObjectInstance,

        [Parameter(Mandatory=$true,ValueFromPipeline=$false)]
        [PsObject] 
        $Template
        )
    Process
    {
        #Browse all template properties and convert their value in a bound value
        $Template | Get-Member -MemberType NoteProperty | ForEach-Object {
            $shortDefinition = ($_.Definition -split " ")[0].Trim()
            #Fix Powershell V5 / V4
            $shortDefinition = $shortDefinition.replace("System.Object[]","Object[]")
            $shortDefinition = $shortDefinition.replace("System.Collections.Hashtable","hashtable")

            $name = $_.Name
            Write-Verbose "Identiy property named '$name' with definition type '$shortDefinition'"

            switch($shortDefinition)
            {
                "object[]" {
                    $ObjectInstance | Add-Member -MemberType NoteProperty -Name $name -Value @()
                    foreach($childElement in $Template.$name)
                    {
                        ConvertTo-Instance -Template $childElement -DataContext $ObjectInstance.BindingSource -ParentObject $ObjectInstance -ParentPropertyName $name
                    }

                }
                "hashtable" {
                    $ObjectInstance | Add-Member -MemberType NoteProperty -Name $name -Value @{}
                    $keys = $Template.$name.keys
                    foreach($key in $keys)
                    {
                        ConvertTo-Instance -Template $Template.$name[$key] -DataContext $ObjectInstance.BindingSource -ParentObject $ObjectInstance -ParentPropertyName $name
                    }

                }
				"System.Management.Automation.PSCustomObject" {
                    $value = ConvertTo-Instance -Template $Template.$name -DataContext $ObjectInstance.BindingSource -ParentObject $ObjectInstance -ParentPropertyName $name

				}
                default {
                    Bind-Property -Template $Template -ObjectInstance $ObjectInstance -PropertyName $name
                }
            }
            
        }
    }

}


function Bind-Property
{
   [cmdletbinding(DefaultParameterSetName="none")]
    Param( 
        [Parameter(Mandatory=$true,ValueFromPipeline=$false)]
        [PsObject] 
        $ObjectInstance,

        [Parameter(Mandatory=$true,ValueFromPipeline=$false)]
        [PsObject] 
        $Template,

        [Parameter(Mandatory=$true,ValueFromPipeline=$false)]
        [String] 
        $PropertyName

        )
    Process
    {
        $extractBindingPattern ="(?<=\{templateBindingValue).+?(?=\})"

        $checkValue = $Template.$PropertyName

        #Check if property have a binding expression or a direct value
        if ($checkValue -match $extractBindingPattern)
        {
            if ($Matches.Count -gt 1)
            {
                Throw "Only one binding expression can be used in template definition"
            }
            $BindExpressions = $Matches[0].trim().Split(",",[System.StringSplitOptions]::RemoveEmptyEntries)
            for($i=0;$i -lt $BindExpressions.Count;$i++)
            {
                $BindExpressions[$i] = $BindExpressions[$i].Trim()
            }
            

            Write-Debug "Bind-Property : Matches - $($Matches[0])"
            Write-Debug "Bind-Property : BindExpressions - $BindExpressions"


            <#Bindin Expression can have :
            Path : to indicate the targeted property to find in BindingSource. Default is the same property name
            Source : to indicate the data source of binding. Default is current object instance BindingSource
            Default : To indicate a default value if property doesn't exist in the binding source
            Convert : To indicate the scripts to call to convert binding source value in the two way
            #>

            $source = Extract-BindingExpression -BindingExpressions $BindExpressions -searchPattern "source" -DefaultValue "BindingSource"
            $Path = Extract-BindingExpression -BindingExpressions $BindExpressions -searchPattern "path" -DefaultValue $PropertyName
            $default = Extract-BindingExpression -BindingExpressions $BindExpressions -searchPattern "default"
            $Convert = Extract-BindingExpression -BindingExpressions $BindExpressions -searchPattern "convert"
            

            $binding = New-Object PSObject
            $uniqueId = [System.Guid]::NewGuid()
            $binding | Add-Member -MemberType NoteProperty -Name Id -Value $uniqueId.Guid
            $binding | Add-Member -MemberType NoteProperty -Name BindingSource -Value $ObjectInstance.$source
            $binding | Add-Member -MemberType NoteProperty -Name Path -Value $path
            $binding | Add-Member -MemberType NoteProperty -Name Default -Value $default
            $binding | Add-Member -MemberType NoteProperty -Name Convert -Value $Convert


            $ObjectInstance.BindingCollections.Add($uniqueId.Guid, $binding)
            Write-Debug "Bind-Property : checkValue - $checkValue"
            $replaceValue = "templateBindingValue$($Matches[0])"
            Write-Debug "Bind-Property : replaceValue - $replaceValue"


            if ($Convert)
            {
                $dynamicValue = "Get-BindingValue -BindingCollections `$this.BindingCollections -BindingProperty $($uniqueId.Guid)"
            }
            else
            {
                $dynamicValue = "'$($checkValue.replace($replaceValue,'0'))' -f (Get-BindingValue -BindingCollections `$this.BindingCollections -BindingProperty $($uniqueId.Guid))"
            }
            Write-Debug "Bind-Property : dynamicValue - $dynamicValue"

            #TODO : Change fonction pour conserver la partie statique présente dans une valeur
            $dynamicGet = "`$ObjectInstance | Add-Member -MemberType ScriptProperty -Name $PropertyName -Value {$dynamicValue}"
            #$dynamicGet = "`$ObjectInstance | Add-Member -MemberType ScriptProperty -Name $PropertyName -Value {write-output '$dynamicValue'}"

            $ExecutionContext.InvokeCommand.InvokeScript($dynamicGet)

        }
        else
        {
            $ObjectInstance | Add-Member -MemberType NoteProperty -Name $PropertyName -Value $checkValue
        }

    }

}


function Extract-BindingExpression
{
    [cmdletbinding(DefaultParameterSetName="none")]
    Param( 
        [Parameter(Mandatory=$true,ValueFromPipeline=$false)]
        [String[]] 
        $BindingExpressions,

        [Parameter(Mandatory=$true,ValueFromPipeline=$false)]
        [PsObject] 
        $searchPattern,

        [Parameter(Mandatory=$false,ValueFromPipeline=$false)]
        [String] 
        $DefaultValue

        )
    Process
    {
        $searchExpression = $BindExpressions | ? {$_ -like "$searchPattern=*"}
        switch($searchExpression.Count)
        {
            0 {
                if ($DefaultValue) 
                {
                    #return default value
                    $DefaultValue
                }
                else {
                    #throw "searchPattern '$searchPattern' is missing in the binding expression '$($BindingExpressions | Out-String)' and no default value has been implemented"
                    $null
                }
            }
            1 {
                $extractValues=$searchExpression.trim().Split("=",[System.StringSplitOptions]::RemoveEmptyEntries)
                if ($extractValues.count -ne 2) {throw "'$searchPattern' grammar is incorrect in '$($BindingExpressions | Out-String)'"}

                #return searchPattern value
                $extractValues[1]

            }
            default {
                    throw "searchPattern '$searchPattern' hs been found more than 1 occurence expression '$($BindingExpressions | Out-String)'"                
            }

        }
        
    }    
}


function Get-BindingValue
{
    [cmdletbinding(DefaultParameterSetName="none")]
    Param( 
        [Parameter(Mandatory=$true,ValueFromPipeline=$false)]
        [System.Collections.Hashtable] 
        $BindingCollections,

        [Parameter(Mandatory=$true,ValueFromPipeline=$false)]
        [String] 
        $BindingProperty
        )
    Process
    {
        if (!$BindingCollections.ContainsKey($BindingProperty))
        {
            Write-Error "Binding property '$BindingProperty' doesn't exist in the BindingCollection"
            if ($BindingObject.Convert)
            {
                Write-Error "Convert feature is not yet implemented, return null"
                return $null
            }
            else
            {
                #return the binding value
                return $null
            }
        }

        $BindingObject = $BindingCollections.$BindingProperty

        if (!$BindingObject.BindingSource)
        {
            if ($BindingObject.Convert)
            {
                Write-Error "Convert feature is not yet implemented, return null"
                return $null
            }
            else
            {
                #return the binding value
                return $null
            }
        }

        $bindingSource = $BindingObject.BindingSource

        $existBindingPath = $bindingSource | Get-Member -Name $BindingObject.Path

        $bindingValue = $null
        if (!$existBindingPath)
        { 
            if ($BindingObject.Default) {$bindingValue = $BindingObject.Default}
            else
            {
                Write-Error "Path '$($BindingObject.Path)' doesn' exist on the binding source '$($bindingSource)'`nand no default value exist"
            }
        }
        else
        {
            $bindingValue = $BindingSource.$($BindingObject.Path)
        }
        


        if ($BindingObject.Convert)
        {
            #Write-Error "Convert feature is not yet implemented, return $bindingValue"
            $commandConvert = "$($BindingObject.Convert) $bindingValue"
            Invoke-Expression $commandConvert
        }
        else
        {
            #return the binding value
            $bindingValue
        }

    }

}


Function ConvertTo-Boolean
{
    param(
    [Object]$value
    )
    process
    {
        $value -eq $true
    }

}